part of 'page.dart';

class _SpacedRepetitionPageWloc
    extends StatefulWloc<_SpacedRepetitionPageWlocState, SpacedRepetitionPage>
    with TickerProviderStateMixin, CardWlocMixin {
  late final bloc.SpacedRepetitionPageBloc _bloc;

  late final AnimationController _clockwiseSlideAnimationController;
  late final Animation<double> clockwiseSlideAnimation;

  late final AnimationController _counterclockwiseSlideAnimationController;
  late final Animation<double> counterclockwiseSlideAnimation;

  late final AnimationController _crossFadeAnimationController;
  late final Animation<double> crossFadeOutAnimation;

  late final AnimationController _leftSlideAnimationController;
  late final Animation<Offset> leftSlideOutAnimation;

  late final AnimationController _disappearanceAnimationController;
  late final Animation<double> disappearanceFadeOutAnimation;

  late final Tween<double> nextCardScaleTween;
  late final CurveTween easeOutTween;
  late final Tween<double> invertedTween;
  late final Tween<Offset> leftSlideInTween;

  late final ValueNotifier<AnimationController?> cardChangeAnimationControllerNotifier;

  FocusNode? currentTypingCardFocusNode;
  FocusNode? nextTypingCardFocusNode;

  String? _lastPerfomedActionCardId;

  @override
  _SpacedRepetitionPageWlocState get initialState {
    final currentTrainableLexicalItemPickingProgress =
        _getCurrentTrainableLexicalItemPickingProgress(
      trainingSize: _bloc.state.trainingSize,
      pickedTrainableLexicalItemIds: _bloc.state.pickedTrainableLexicalItemIds,
    );

    return _SpacedRepetitionPageWlocState(
      currentCardInfo: _bloc.state.currentCardInfo,
      nextCardInfo: null,
      dialect: _bloc.state.dialect,
      autoPronunciationEnabled: _bloc.state.autoPronunciationEnabled,
      trainingSize: _bloc.state.trainingSize,
      currentTrainableLexicalItemPickingProgress: currentTrainableLexicalItemPickingProgress,
      previousTrainableLexicalItemPickingProgress: null,
    );
  }

  double _getCurrentTrainableLexicalItemPickingProgress({
    required int trainingSize,
    required BuiltSet<String> pickedTrainableLexicalItemIds,
  }) {
    return pickedTrainableLexicalItemIds.length / trainingSize;
  }

  double _getPreviousTrainableLexicalItemPickingProgress({
    required int trainingSize,
    required BuiltSet<String> pickedTrainableLexicalItemIds,
    required BuiltSet<String> previousPickedTrainableLexicalItemIds,
  }) {
    final unpickedLexicalItemIds =
        previousPickedTrainableLexicalItemIds.difference(pickedTrainableLexicalItemIds);

    return (previousPickedTrainableLexicalItemIds.length - unpickedLexicalItemIds.length) /
        trainingSize;
  }

  void _showLexicalItemEditingDialog({
    required String lexicalItemId,
  }) {
    showAppFullscreenModal(
      enableDrag: false,
      builder: (context) {
        return CustomLexicalItemEditingPage(
          customLexicalItemId: lexicalItemId,
        );
      },
    );
  }

  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// UnspecifiedLexicalItemObscureCard
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // <editor-fold defaultstate="collapsed">

  void onUnspecifiedLexicalItemObscureCardExposureButtonPressed() {
    _bloc.requestUnspecifiedLexicalItemObscureCardExposure();
  }

  // </editor-fold>

  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// UnspecifiedLexicalItemExposedCard
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // <editor-fold defaultstate="collapsed">

  void onUnspecifiedLexicalItemExposedCardMarkAsKnownButtonPressed() {
    _bloc.requestUnspecifiedLexicalItemExposedCardLexicalItemMarkAsKnown();
  }

  void onUnspecifiedLexicalItemExposedCardMarkAsUnknownButtonPressed() {
    _bloc.requestUnspecifiedLexicalItemExposedCardLexicalItemMarkAsUnknown();
  }

  void onUnspecifiedLexicalItemExposedCardMoreActionButtonPressed() async {
    final currentCardInfo = state.currentCardInfo as UnspecifiedLexicalItemExposedCardInfo;

    final cardAction = await showAppActionDialog<CardAction>(
      context: context,
      builder: (context) {
        return AppActionDialog(
          actions: currentCardInfo.cardActions.map((cardAction) {
            return switch (cardAction) {
              SkipCardAction() => _buildUnspecifiedLexicalItemExposedCardSkipCardAction(
                cardAction: cardAction,
                context: context,
              ),
              MistakeReportCardAction() => buildMistakeReportCardAction(
                cardAction: cardAction,
                context: context,
              ),
              AdditionToCustomCollectionCardAction() => buildAdditionToCustomCollectionCardAction(
                cardAction: cardAction,
                context: context,
              ),
              ExclusionFromLearningCardAction() => buildExclusionFromLearningCardAction(
                cardAction: cardAction,
                context: context,
              ),
              EditingCardAction() => buildEditingCardAction(
                cardAction: cardAction,
                context: context,
              ),
              _ => throw UnimplementedError(),
            };
          }).toList(),
          onCancelButtonPressed: Navigator.of(context).pop,
        );
      },
    );

    /// ignore: use_build_context_synchronously
    if (!context.mounted) {
      return;
    }

    switch (cardAction) {
      case SkipCardAction():
        _onUnspecifiedLexicalItemExposedCardSkipCardActionPressed(
          cardAction: cardAction,
        );

      case MistakeReportCardAction():
        showMistakeReportDialog(
          context: context,
          cardAction: cardAction,
          onReportThroughTelegram: () {
            _onUnspecifiedLexicalItemExposedCardMistakeReportThroughTelegramCardActionPressed(
              cardAction: cardAction,
            );
          },
          onReportByEmail: () {
            _onUnspecifiedLexicalItemExposedCardMistakeReportByEmailCardActionPressed(
              cardAction: cardAction,
            );
          },
        );

      case AdditionToCustomCollectionCardAction():
        _onUnspecifiedLexicalItemExposedCardAdditionToCustomCollectionCardActionPressed(
          cardAction: cardAction,
        );

      case ExclusionFromLearningCardAction():
        _onUnspecifiedLexicalItemExposedCardExclusionFromLearningCardActionPressed(
          cardAction: cardAction,
        );

      case EditingCardAction():
        _showLexicalItemEditingDialog(
          lexicalItemId: currentCardInfo.lexicalItem.id,
        );
    }
  }

  void _onUnspecifiedLexicalItemExposedCardSkipCardActionPressed({
    required SkipCardAction cardAction,
  }) {
    _bloc.requestUnspecifiedLexicalItemExposedCardSkip();
  }

  void _onUnspecifiedLexicalItemExposedCardExclusionFromLearningCardActionPressed({
    required ExclusionFromLearningCardAction cardAction,
  }) {
    _bloc.requestUnspecifiedLexicalItemExposedCardLexicalItemExclusionFromLearning();
  }

  AppActionDialogAction _buildUnspecifiedLexicalItemExposedCardSkipCardAction({
    required SkipCardAction cardAction,
    required BuildContext context,
  }) {
    return AppActionDialogAction(
      title: SpacedRepetitionPageTranslation.of(context).skipCardActionTitle,
      showPremiumBadge: !cardAction.permitted,
      onPressed: () {
        Navigator.of(context).pop(cardAction);
      },
    );
  }

  void _onUnspecifiedLexicalItemExposedCardMistakeReportThroughTelegramCardActionPressed({
    required MistakeReportCardAction cardAction,
  }) {
    _bloc.requestUnspecifiedLexicalItemExposedCardLexicalItemMistakeReportThroughTelegram();
  }

  void _onUnspecifiedLexicalItemExposedCardMistakeReportByEmailCardActionPressed({
    required MistakeReportCardAction cardAction,
  }) {
    final availableMailApps = cardAction.availableEmailApps;

    if (availableMailApps.isEmpty) {
      _bloc.requestUnspecifiedLexicalItemExposedCardMistakeReportByEmail(
        emailApp: null,
      );
    } else if (availableMailApps.length == 1) {
      _bloc.requestUnspecifiedLexicalItemExposedCardMistakeReportByEmail(
        emailApp: availableMailApps.first,
      );
    } else {
      showMailAppChoosingDialog(
        context: context,
        availableMailApps: availableMailApps,
        onEmailAppChosen: (chosenEmailApp) {
          _bloc.requestUnspecifiedLexicalItemExposedCardMistakeReportByEmail(
            emailApp: chosenEmailApp,
          );
        },
      );
    }
  }

  void _onUnspecifiedLexicalItemExposedCardAdditionToCustomCollectionCardActionPressed({
    required AdditionToCustomCollectionCardAction cardAction,
  }) {
    if (cardAction.collectionInfos.length == 1) {
      final collectionInfo = cardAction.collectionInfos.first;
      final customCollectionId = collectionInfo.customCollection.id;

      _onUnspecifiedLexicalItemExposedCardAdditionToSingleCollectionCardActionPressed(
        customCollectionId: customCollectionId,
        cardAction: cardAction,
      );
    } else {
      showAdditionToCollectionDialog(
        context: context,
        cardAction: cardAction,
        onAddToCustomCollection: (customCollectionId) {
          _onUnspecifiedLexicalItemExposedCardAdditionToSingleCollectionCardActionPressed(
            customCollectionId: customCollectionId,
            cardAction: cardAction,
          );
        },
      );
    }
  }

  void _onUnspecifiedLexicalItemExposedCardAdditionToSingleCollectionCardActionPressed({
    required String customCollectionId,
    required AdditionToCustomCollectionCardAction cardAction,
  }) {
    _bloc.requestUnspecifiedLexicalItemExposedCardLexicalItemAdditionToCustomCollection(
      customCollectionId: customCollectionId,
    );
  }

  // </editor-fold>

  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// RepetitionAnswerSelectionObscureCard
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // <editor-fold defaultstate="collapsed">

  void onRepetitionAnswerSelectionObscureCardExposureButtonPressed() {
    _bloc.requestRepetitionAnswerSelectionObscureCardExposure();
  }

  void onRepetitionAnswerSelectionObscureCardTypingModeEnablementButtonPressed() {
    _bloc.requestRepetitionAnswerSelectionObscureCardTypingModeEnablement();
  }

  // </editor-fold>

  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// RepetitionAnswerSelectionExposedCard
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // <editor-fold defaultstate="collapsed">

  void onRepetitionAnswerSelectionExposedMarkAsRememberedButtonPressed() {
    _bloc.requestRepetitionAnswerSelectionExposedCardLexicalItemMarkAsRemembered();
  }

  void onRepetitionAnswerSelectionExposedMarkAsBadlyRememberedButtonPressed() {
    _bloc.requestRepetitionAnswerSelectionExposedCardLexicalItemMarkAsBadlyRemembered();
  }

  void onRepetitionAnswerSelectionExposedMarkAsUnrememberedButtonPressed() {
    _bloc.requestRepetitionAnswerSelectionExposedCardLexicalItemMarkAsUnremembered();
  }

  void onRepetitionAnswerSelectionExposedCardMoreActionButtonPressed() async {
    final currentCardInfo = state.currentCardInfo as RepetitionAnswerSelectionExposedCardInfo;

    final cardAction = await showAppActionDialog<CardAction>(
      context: context,
      builder: (context) {
        return AppActionDialog(
          actions: currentCardInfo.cardActions.map((cardAction) {
            return switch (cardAction) {
              MistakeReportCardAction() => buildMistakeReportCardAction(
                cardAction: cardAction,
                context: context,
              ),
              ProgressResetCardAction() => buildProgressResetCardAction(
                cardAction: cardAction,
                context: context,
              ),
              MarkAsCompletelyLearnedCardAction() => buildMarkAsCompletelyLearnedCardAction(
                cardAction: cardAction,
                context: context,
              ),
              AdditionToCustomCollectionCardAction() => buildAdditionToCustomCollectionCardAction(
                cardAction: cardAction,
                context: context,
              ),
              ExclusionFromLearningCardAction() => buildExclusionFromLearningCardAction(
                cardAction: cardAction,
                context: context,
              ),
              EditingCardAction() => buildEditingCardAction(
                cardAction: cardAction,
                context: context,
              ),
              _ => throw UnimplementedError(),
            };
          }).toList(),
          onCancelButtonPressed: Navigator.of(context).pop,
        );
      },
    );

    // ignore: use_build_context_synchronously
    if (!context.mounted) {
      return;
    }

    switch (cardAction) {
      case MistakeReportCardAction():
        showMistakeReportDialog(
          context: context,
          cardAction: cardAction,
          onReportThroughTelegram: () {
            _onRepetitionAnswerSelectionExposedCardMistakeReportThroughTelegramCardActionPressed(
              cardAction: cardAction,
            );
          },
          onReportByEmail: () {
            _onRepetitionAnswerSelectionExposedCardMistakeReportByEmailCardActionPressed(
              cardAction: cardAction,
            );
          },
        );

      case ProgressResetCardAction():
        _onRepetitionAnswerSelectionExposedCardProgressResetCardActionPressed(
          cardAction: cardAction,
        );

      case MarkAsCompletelyLearnedCardAction():
        _onRepetitionAnswerSelectionExposedCardMarkAsCompletelyLearnedCardActionPressed(
          cardAction: cardAction,
        );

      case AdditionToCustomCollectionCardAction():
        _onRepetitionAnswerSelectionExposedCardAdditionToCustomCollectionCardActionPressed(
          cardAction: cardAction,
        );

      case ExclusionFromLearningCardAction():
        _onRepetitionAnswerSelectionExposedCardExclusionFromLearningCardActionPressed(
          cardAction: cardAction,
        );

      case EditingCardAction():
        _showLexicalItemEditingDialog(
          lexicalItemId: currentCardInfo.lexicalItem.id,
        );
    }
  }

  void _onRepetitionAnswerSelectionExposedCardMistakeReportByEmailCardActionPressed({
    required MistakeReportCardAction cardAction,
  }) {
    final availableMailApps = cardAction.availableEmailApps;

    if (availableMailApps.isEmpty) {
      _bloc.requestRepetitionAnswerSelectionExposedCardMistakeReportByEmail(
        emailApp: null,
      );
    } else if (availableMailApps.length == 1) {
      _bloc.requestRepetitionAnswerSelectionExposedCardMistakeReportByEmail(
        emailApp: availableMailApps.first,
      );
    } else {
      showMailAppChoosingDialog(
        context: context,
        availableMailApps: availableMailApps,
        onEmailAppChosen: (chosenEmailApp) {
          _bloc.requestRepetitionAnswerSelectionExposedCardMistakeReportByEmail(
            emailApp: chosenEmailApp,
          );
        },
      );
    }
  }

  void _onRepetitionAnswerSelectionExposedCardProgressResetCardActionPressed({
    required ProgressResetCardAction cardAction,
  }) {
    _bloc.requestRepetitionAnswerSelectionExposedCardLexicalItemProgressReset();
  }

  void _onRepetitionAnswerSelectionExposedCardMarkAsCompletelyLearnedCardActionPressed({
    required MarkAsCompletelyLearnedCardAction cardAction,
  }) {
    _bloc.requestRepetitionAnswerSelectionExposedCardLexicalItemMarkAsCompletelyLearned();
  }

  void _onRepetitionAnswerSelectionExposedCardMistakeReportThroughTelegramCardActionPressed({
    required MistakeReportCardAction cardAction,
  }) {
    _bloc.requestRepetitionAnswerSelectionExposedCardLexicalItemMistakeReportThroughTelegram();
  }

  void _onRepetitionAnswerSelectionExposedCardAdditionToCustomCollectionCardActionPressed({
    required AdditionToCustomCollectionCardAction cardAction,
  }) {
    if (cardAction.collectionInfos.length == 1) {
      final collectionInfo = cardAction.collectionInfos.first;
      final customCollectionId = collectionInfo.customCollection.id;

      _onRepetitionAnswerSelectionExposedCardAdditionToSingleCollectionCardActionPressed(
        customCollectionId: customCollectionId,
        cardAction: cardAction,
      );
    } else {
      showAdditionToCollectionDialog(
        context: context,
        cardAction: cardAction,
        onAddToCustomCollection: (customCollectionId) {
          _onRepetitionAnswerSelectionExposedCardAdditionToSingleCollectionCardActionPressed(
            customCollectionId: customCollectionId,
            cardAction: cardAction,
          );
        },
      );
    }
  }

  void _onRepetitionAnswerSelectionExposedCardAdditionToSingleCollectionCardActionPressed({
    required String customCollectionId,
    required AdditionToCustomCollectionCardAction cardAction,
  }) {
    _bloc.requestRepetitionAnswerSelectionExposedCardLexicalItemAdditionToCustomCollection(
      customCollectionId: customCollectionId,
    );
  }

  void _onRepetitionAnswerSelectionExposedCardExclusionFromLearningCardActionPressed({
    required ExclusionFromLearningCardAction cardAction,
  }) {
    _bloc.requestRepetitionAnswerSelectionExposedCardLexicalItemExclusionFromLearning();
  }

  // </editor-fold>

  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// RepetitionAnswerTypingCard
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // <editor-fold defaultstate="collapsed">

  void onRepetitionAnswerTypingCardHintUsed() {
    _bloc.requestRepetitionAnswerTypingCardHintUsageHandling();
  }

  void onRepetitionAnswerTypingCardLexicalItemTypedCorrectly() {
    _bloc.requestRepetitionAnswerTypingCardCorrectAnswerHandling();
  }

  void onRepetitionAnswerTypingCardMarkAsUnrememberedButtonPressed() {
    _bloc.requestRepetitionAnswerTypingCardLexicalItemMarkAsUnremembered();
  }

  void onRepetitionAnswerTypingCardTypingModeDisablementButtonPressed() {
    _bloc.requestRepetitionAnswerTypingCardTypingModeDisablement();
  }

  // </editor-fold>

  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// MemorizationCard
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // <editor-fold defaultstate="collapsed">

  void onMemorizationCardClosingButtonPressed() {
    _bloc.requestMemorizationCardClosing();
  }

  void onMemorizationCardMoreActionButtonPressed() async {
    final currentCardInfo = state.currentCardInfo as MemorizationCardInfo;

    final cardAction = await showAppActionDialog<CardAction>(
      context: context,
      builder: (context) {
        return AppActionDialog(
          actions: currentCardInfo.cardActions.map((cardAction) {
            return switch (cardAction) {
              MistakeReportCardAction() => buildMistakeReportCardAction(
                cardAction: cardAction,
                context: context,
              ),
              ProgressResetCardAction() => buildProgressResetCardAction(
                cardAction: cardAction,
                context: context,
              ),
              AdditionToCustomCollectionCardAction() => buildAdditionToCustomCollectionCardAction(
                cardAction: cardAction,
                context: context,
              ),
              ExclusionFromLearningCardAction() => buildExclusionFromLearningCardAction(
                cardAction: cardAction,
                context: context,
              ),
              EditingCardAction() => buildEditingCardAction(
                cardAction: cardAction,
                context: context,
              ),
              _ => throw UnimplementedError(),
            };
          }).toList(),
          onCancelButtonPressed: Navigator.of(context).pop,
        );
      },
    );

    // ignore: use_build_context_synchronously
    if (!context.mounted) {
      return;
    }

    switch (cardAction) {
      case MistakeReportCardAction():
        showMistakeReportDialog(
          context: context,
          cardAction: cardAction,
          onReportThroughTelegram: () {
            _onMemorizationCardMistakeReportThroughTelegramCardActionPressed(
              cardAction: cardAction,
            );
          },
          onReportByEmail: () {
            _onMemorizationCardMistakeReportByEmailCardActionPressed(
              cardAction: cardAction,
            );
          },
        );

      case ProgressResetCardAction():
        _onMemorizationCardProgressResetCardActionPressed(
          cardAction: cardAction,
        );

      case AdditionToCustomCollectionCardAction():
        _onMemorizationCardAdditionToCustomCollectionCardActionPressed(
          cardAction: cardAction,
        );

      case ExclusionFromLearningCardAction():
        _onMemorizationCardExclusionFromLearningCardActionPressed(
          cardAction: cardAction,
        );

      case EditingCardAction():
        _showLexicalItemEditingDialog(
          lexicalItemId: currentCardInfo.lexicalItem.id,
        );
    }
  }

  void _onMemorizationCardMistakeReportByEmailCardActionPressed({
    required MistakeReportCardAction cardAction,
  }) {
    final availableMailApps = cardAction.availableEmailApps;

    if (availableMailApps.isEmpty) {
      _bloc.requestMemorizationCardMistakeReportByEmail(
        emailApp: null,
      );
    } else if (availableMailApps.length == 1) {
      _bloc.requestMemorizationCardMistakeReportByEmail(
        emailApp: availableMailApps.first,
      );
    } else {
      showMailAppChoosingDialog(
        context: context,
        availableMailApps: availableMailApps,
        onEmailAppChosen: (chosenEmailApp) {
          _bloc.requestMemorizationCardMistakeReportByEmail(
            emailApp: chosenEmailApp,
          );
        },
      );
    }
  }

  void _onMemorizationCardProgressResetCardActionPressed({
    required ProgressResetCardAction cardAction,
  }) {
    _bloc.requestMemorizationCardLexicalItemProgressReset();
  }

  void _onMemorizationCardMistakeReportThroughTelegramCardActionPressed({
    required MistakeReportCardAction cardAction,
  }) {
    _bloc.requestMemorizationCardLexicalItemMistakeReportThroughTelegram();
  }

  void _onMemorizationCardAdditionToCustomCollectionCardActionPressed({
    required AdditionToCustomCollectionCardAction cardAction,
  }) {
    if (cardAction.collectionInfos.length == 1) {
      final collectionInfo = cardAction.collectionInfos.first;
      final customCollectionId = collectionInfo.customCollection.id;

      _onMemorizationCardAdditionToSingleCollectionCardActionPressed(
        customCollectionId: customCollectionId,
        cardAction: cardAction,
      );
    } else {
      showAdditionToCollectionDialog(
        context: context,
        cardAction: cardAction,
        onAddToCustomCollection: (customCollectionId) {
          _onMemorizationCardAdditionToSingleCollectionCardActionPressed(
            customCollectionId: customCollectionId,
            cardAction: cardAction,
          );
        },
      );
    }
  }

  void _onMemorizationCardAdditionToSingleCollectionCardActionPressed({
    required String customCollectionId,
    required AdditionToCustomCollectionCardAction cardAction,
  }) {
    _bloc.requestMemorizationCardLexicalItemAdditionToCustomCollection(
      customCollectionId: customCollectionId,
    );
  }

  void _onMemorizationCardExclusionFromLearningCardActionPressed({
    required ExclusionFromLearningCardAction cardAction,
  }) {
    _bloc.requestMemorizationCardLexicalItemExclusionFromLearning();
  }

  // </editor-fold>

  void _resetAnimationControllers() {
    _clockwiseSlideAnimationController.reset();
    _counterclockwiseSlideAnimationController.reset();
    _crossFadeAnimationController.reset();
    _leftSlideAnimationController.reset();
    _disappearanceAnimationController.reset();

    _setCardChangeAnimationController(null);
  }

  void _setCardChangeAnimationController(AnimationController? animationController) {
    if (Config().platform.isAndroid) {
      return;
    }

    cardChangeAnimationControllerNotifier.value
        ?.removeListener(_handleCardChangAnimationControllerUpdate);

    cardChangeAnimationControllerNotifier.value = animationController;

    cardChangeAnimationControllerNotifier.value
        ?.addListener(_handleCardChangAnimationControllerUpdate);
  }

  void _handleTypingCardChangeFocusNodeUpdate({
    required CardInfo blocCurrentCardInfo,
  }) {
    if (state.currentCardInfo is RepetitionAnswerTypingCardInfo) {
      final focusNodeToDispose = currentTypingCardFocusNode!;
      currentTypingCardFocusNode = null;

      WidgetsBinding.instance.addPostFrameCallback((_) {
        focusNodeToDispose.dispose();
      });
    }

    if (blocCurrentCardInfo is RepetitionAnswerTypingCardInfo) {
      final focusNodeToDispose = nextTypingCardFocusNode;

      currentTypingCardFocusNode = FocusNode();
      currentTypingCardFocusNode!.requestFocus();
      nextTypingCardFocusNode = null;

      WidgetsBinding.instance.addPostFrameCallback((_) {
        focusNodeToDispose?.dispose();
      });
    }
  }

  void _handleCurrentCardChange({
    required bloc.SpacedRepetitionPageBlocState blocState,
    required bool trainingProgressChanged,
    required double currentTrainableLexicalItemPickingProgress,
    required double previousTrainableLexicalItemPickingProgress,
  }) {
    _resetAnimationControllers();

    final blocCurrentCardInfo = blocState.currentCardInfo;
    final blocPreviousCardInfo = blocState.previousCardInfo;

    final wlocCurrentCardInfo = state.currentCardInfo;

    if (blocPreviousCardInfo == null || wlocCurrentCardInfo.cardId != blocPreviousCardInfo.cardId) {
      _handleTypingCardChangeFocusNodeUpdate(
        blocCurrentCardInfo: blocCurrentCardInfo,
      );

      var updatedState = state.copyWith(
        currentCardInfo: () => blocCurrentCardInfo,
        nextCardInfo: () => null,
        autoPronunciationEnabled: () => blocState.autoPronunciationEnabled,
        dialect: () => blocState.dialect,
      );

      if (trainingProgressChanged) {
        updatedState = updatedState.copyWith(
          trainingSize: () => blocState.trainingSize,
          currentTrainableLexicalItemPickingProgress: () =>
              currentTrainableLexicalItemPickingProgress,
          previousTrainableLexicalItemPickingProgress: () =>
              previousTrainableLexicalItemPickingProgress,
        );
      }

      emit(updatedState);

      _maybePerformBlocCurrentCardAction();

      return;
    }

    if (blocCurrentCardInfo is RepetitionAnswerTypingCardInfo) {
      nextTypingCardFocusNode = FocusNode();
    }

    switch (blocPreviousCardInfo) {
      case UnspecifiedLexicalItemObscureCardInfo():
        switch (blocPreviousCardInfo.finishEvent) {
          case UnspecifiedLexicalItemObscureCardFinishEvent.exposed:
            _setCardChangeAnimationController(_crossFadeAnimationController);

          case UnspecifiedLexicalItemObscureCardFinishEvent.vanished:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case UnspecifiedLexicalItemObscureCardFinishEvent.none:
            throw UnimplementedError();
        }

      case UnspecifiedLexicalItemExposedCardInfo():
        switch (blocPreviousCardInfo.finishEvent) {
          case UnspecifiedLexicalItemExposedCardFinishEvent.markedAsKnown:
            _setCardChangeAnimationController(_counterclockwiseSlideAnimationController);

          case UnspecifiedLexicalItemExposedCardFinishEvent.markedAsUnknown:
            _setCardChangeAnimationController(_clockwiseSlideAnimationController);

          case UnspecifiedLexicalItemExposedCardFinishEvent.skipped:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case UnspecifiedLexicalItemExposedCardFinishEvent.progressReset:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case UnspecifiedLexicalItemExposedCardFinishEvent.excludedFromLearning:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case UnspecifiedLexicalItemExposedCardFinishEvent.vanished:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case UnspecifiedLexicalItemExposedCardFinishEvent.none:
            throw UnimplementedError();
        }

      case RepetitionAnswerSelectionObscureCardInfo():
        switch (blocPreviousCardInfo.finishEvent) {
          case RepetitionAnswerSelectionObscureCardFinishEvent.exposed:
            _setCardChangeAnimationController(_crossFadeAnimationController);

          case RepetitionAnswerSelectionObscureCardFinishEvent.typingModeEnabled:
            _setCardChangeAnimationController(_crossFadeAnimationController);

          case RepetitionAnswerSelectionObscureCardFinishEvent.vanished:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case RepetitionAnswerSelectionObscureCardFinishEvent.none:
            throw UnimplementedError();
        }

      case RepetitionAnswerSelectionExposedCardInfo():
        switch (blocPreviousCardInfo.finishEvent) {
          case RepetitionAnswerSelectionExposedCardFinishEvent.markedAsRemembered:
            _setCardChangeAnimationController(_counterclockwiseSlideAnimationController);

          case RepetitionAnswerSelectionExposedCardFinishEvent.markedAsBadlyRemembered:
            _setCardChangeAnimationController(_clockwiseSlideAnimationController);

          case RepetitionAnswerSelectionExposedCardFinishEvent.markedAsUnremembered:
            _setCardChangeAnimationController(_clockwiseSlideAnimationController);

          case RepetitionAnswerSelectionExposedCardFinishEvent.markedAsCompletelyLearned:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case RepetitionAnswerSelectionExposedCardFinishEvent.progressReset:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case RepetitionAnswerSelectionExposedCardFinishEvent.excludedFromLearning:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case RepetitionAnswerSelectionExposedCardFinishEvent.vanished:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case RepetitionAnswerSelectionExposedCardFinishEvent.none:
            throw UnimplementedError();
        }

      case RepetitionAnswerTypingCardInfo():
        switch (blocPreviousCardInfo.finishEvent) {
          case RepetitionAnswerTypingCardFinishEvent.typingModeDisabled:
            assert(blocCurrentCardInfo is RepetitionAnswerSelectionObscureCardInfo);

            _setCardChangeAnimationController(_crossFadeAnimationController);

          case RepetitionAnswerTypingCardFinishEvent.explicitMarkRequired:
            assert(blocCurrentCardInfo is RepetitionAnswerSelectionExposedCardInfo);

            _setCardChangeAnimationController(_crossFadeAnimationController);

          case RepetitionAnswerTypingCardFinishEvent.markedAsUnremembered:
            assert(blocCurrentCardInfo is MemorizationCardInfo);

            _setCardChangeAnimationController(_crossFadeAnimationController);

          case RepetitionAnswerTypingCardFinishEvent.vanished:
            _setCardChangeAnimationController(_disappearanceAnimationController);

          case RepetitionAnswerTypingCardFinishEvent.none:
            throw UnimplementedError();

          case RepetitionAnswerTypingCardFinishEvent.markedAsRemembered:
            switch (blocCurrentCardInfo) {
              case RepetitionAnswerTypingCardInfo():
                _setCardChangeAnimationController(_leftSlideAnimationController);

              case UnspecifiedLexicalItemObscureCardInfo():
                _setCardChangeAnimationController(_leftSlideAnimationController);

              case DailyLimitCardInfo():
                _setCardChangeAnimationController(_leftSlideAnimationController);

              case LearningFinishCardInfo():
                _setCardChangeAnimationController(_leftSlideAnimationController);

              case RepetitionAnswerSelectionObscureCardInfo():
              case RepetitionAnswerSelectionExposedCardInfo():
              case UnspecifiedLexicalItemExposedCardInfo():
              case MemorizationCardInfo():
                throw UnimplementedError();
            }
        }

        if (blocCurrentCardInfo is RepetitionAnswerTypingCardInfo) {
          nextTypingCardFocusNode!.requestFocus();
        }

      case MemorizationCardInfo():
        switch (blocPreviousCardInfo.finishEvent) {
          case MemorizationCardFinishEvent.closed:
            _setCardChangeAnimationController(_clockwiseSlideAnimationController);
          case MemorizationCardFinishEvent.excludedFromLearning:
            _setCardChangeAnimationController(_disappearanceAnimationController);
          case MemorizationCardFinishEvent.vanished:
            _setCardChangeAnimationController(_disappearanceAnimationController);
          case MemorizationCardFinishEvent.none:
            throw UnimplementedError();
        }

      case DailyLimitCardInfo():
        _setCardChangeAnimationController(_disappearanceAnimationController);

      case LearningFinishCardInfo():
        _setCardChangeAnimationController(_disappearanceAnimationController);
    }

    final cardChangeAnimationController = cardChangeAnimationControllerNotifier.value;

    if (cardChangeAnimationController == null) {
      _handleTypingCardChangeFocusNodeUpdate(
        blocCurrentCardInfo: blocCurrentCardInfo,
      );

      var updatedState = state.copyWith(
        currentCardInfo: () => blocCurrentCardInfo,
        nextCardInfo: () => null,
        autoPronunciationEnabled: () => blocState.autoPronunciationEnabled,
        dialect: () => blocState.dialect,
      );

      if (trainingProgressChanged) {
        updatedState = updatedState.copyWith(
          trainingSize: () => blocState.trainingSize,
          currentTrainableLexicalItemPickingProgress: () =>
              currentTrainableLexicalItemPickingProgress,
          previousTrainableLexicalItemPickingProgress: () =>
              previousTrainableLexicalItemPickingProgress,
        );
      }

      emit(updatedState);

      _maybePerformBlocCurrentCardAction();
    } else {
      var updatedState = state.copyWith(
        currentCardInfo: () => blocPreviousCardInfo,
        nextCardInfo: () => blocCurrentCardInfo,
        autoPronunciationEnabled: () => blocState.autoPronunciationEnabled,
        dialect: () => blocState.dialect,
      );

      if (trainingProgressChanged) {
        updatedState = updatedState.copyWith(
          trainingSize: () => blocState.trainingSize,
          currentTrainableLexicalItemPickingProgress: () =>
              currentTrainableLexicalItemPickingProgress,
          previousTrainableLexicalItemPickingProgress: () =>
              previousTrainableLexicalItemPickingProgress,
        );
      }

      emit(updatedState);

      cardChangeAnimationController.forward(from: 0.0).whenComplete(() {
        _handleTypingCardChangeFocusNodeUpdate(
          blocCurrentCardInfo: blocCurrentCardInfo,
        );

        emit(state.copyWith(
          currentCardInfo: () => blocCurrentCardInfo,
          nextCardInfo: () => null,
        ));

        _resetAnimationControllers();
      });
    }
  }

  void _maybePerformBlocCurrentCardAction() {
    final blocCurrentCardInfo = _bloc.state.currentCardInfo;

    if (_lastPerfomedActionCardId == blocCurrentCardInfo.cardId) {
      return;
    }

    switch (blocCurrentCardInfo) {
      case UnspecifiedLexicalItemObscureCardInfo():
        _bloc.requestUnspecifiedLexicalItemObscureCardLexicalItemAutoPronunciation();

      case RepetitionAnswerSelectionExposedCardInfo():
        _bloc.requestRepetitionAnswerSelectionExposedCardLexicalItemAutoPronunciation();

      case RepetitionAnswerTypingCardInfo():
        if (blocCurrentCardInfo.cardId == state.currentCardInfo.cardId) {
          currentTypingCardFocusNode!.requestFocus();
        } else if (blocCurrentCardInfo.cardId == state.nextCardInfo?.cardId) {
          nextTypingCardFocusNode!.requestFocus();
        }

      case MemorizationCardInfo():
        _bloc.requestMemorizationCardLexicalItemAutoPronunciation();

      case UnspecifiedLexicalItemExposedCardInfo():
      case RepetitionAnswerSelectionObscureCardInfo():
      case DailyLimitCardInfo():
      case LearningFinishCardInfo():
        break;
    }

    _lastPerfomedActionCardId = blocCurrentCardInfo.cardId;
  }

  void _handleCardChangAnimationControllerUpdate() {
    final cardChangeAnimationController = cardChangeAnimationControllerNotifier.value;

    if (cardChangeAnimationController == null) {
      return;
    }

    if (cardChangeAnimationController.value > 0.5) {
      _maybePerformBlocCurrentCardAction();
    }
  }

  void _onBlocStateChanged(
      (bloc.SpacedRepetitionPageBlocState, bloc.SpacedRepetitionPageBlocState) valueWithPrevious) {
    final (blocState, blocPreviousState) = valueWithPrevious;

    final currentTrainableLexicalItemPickingProgress =
        _getCurrentTrainableLexicalItemPickingProgress(
      trainingSize: blocState.trainingSize,
      pickedTrainableLexicalItemIds: blocState.pickedTrainableLexicalItemIds,
    );

    final previousTrainableLexicalItemPickingProgress =
        _getPreviousTrainableLexicalItemPickingProgress(
      trainingSize: blocState.trainingSize,
      pickedTrainableLexicalItemIds: blocState.pickedTrainableLexicalItemIds,
      previousPickedTrainableLexicalItemIds: blocPreviousState.pickedTrainableLexicalItemIds,
    );

    final trainingProgressChanged = blocState.trainingSize != blocPreviousState.trainingSize ||
        blocState.pickedTrainableLexicalItemIds.length !=
            blocPreviousState.pickedTrainableLexicalItemIds.length;

    if (blocState.currentCardInfo.cardId == state.currentCardInfo.cardId) {
      var updatedState = state.copyWith(
        currentCardInfo: () => blocState.currentCardInfo,
        dialect: () => blocState.dialect,
        autoPronunciationEnabled: () => blocState.autoPronunciationEnabled,
      );

      if (trainingProgressChanged) {
        updatedState = updatedState.copyWith(
          trainingSize: () => blocState.trainingSize,
          currentTrainableLexicalItemPickingProgress: () =>
              currentTrainableLexicalItemPickingProgress,
          previousTrainableLexicalItemPickingProgress: () =>
              previousTrainableLexicalItemPickingProgress,
        );
      }

      emit(updatedState);
    } else if (blocState.currentCardInfo.cardId == state.nextCardInfo?.cardId) {
      var updatedState = state.copyWith(
        nextCardInfo: () => blocState.currentCardInfo,
        dialect: () => blocState.dialect,
        autoPronunciationEnabled: () => blocState.autoPronunciationEnabled,
      );

      if (trainingProgressChanged) {
        updatedState = updatedState.copyWith(
          trainingSize: () => blocState.trainingSize,
          currentTrainableLexicalItemPickingProgress: () =>
              currentTrainableLexicalItemPickingProgress,
          previousTrainableLexicalItemPickingProgress: () =>
              previousTrainableLexicalItemPickingProgress,
        );
      }

      emit(updatedState);
    } else {
      _handleCurrentCardChange(
        blocState: blocState,
        trainingProgressChanged: trainingProgressChanged,
        currentTrainableLexicalItemPickingProgress: currentTrainableLexicalItemPickingProgress,
        previousTrainableLexicalItemPickingProgress: previousTrainableLexicalItemPickingProgress,
      );
    }
  }

  @override
  void initState() {
    super.initState();

    _bloc = bloc.SpacedRepetitionPageBloc(
      spacedRepetitionBloc: core_di.sl(),
      learningContentBloc: core_di.sl(),
      userInfoBloc: core_di.sl(),
      rootNavigatorBloc: core_di.sl(),
      overlayBloc: core_di.sl(),
      emailBloc: core_di.sl(),
      urlLauncherBloc: core_di.sl(),
      purchaseFlowDisplayBloc: core_di.sl(),
      settingsBloc: core_di.sl(),
      getInitialData: spaced_repetition_page_di.sl(),
      initialize: spaced_repetition_page_di.sl(),
      addMemorizationCardLexicalItemToCustomCollection: spaced_repetition_page_di.sl(),
      addRepetitionAnswerSelectionExposedCardLexicalItemToCustomCollection:
          spaced_repetition_page_di.sl(),
      addUnspecifiedLexicalItemExposedCardLexicalItemToCustomCollection:
          spaced_repetition_page_di.sl(),
      closeMemorizationCard: spaced_repetition_page_di.sl(),
      disableRepetitionAnswerTypingCardTypingMode: spaced_repetition_page_di.sl(),
      enableRepetitionAnswerSelectionObscureCardTypingMode: spaced_repetition_page_di.sl(),
      excludeMemorizationCardLexicalItemFromLearning: spaced_repetition_page_di.sl(),
      excludeRepetitionAnswerSelectionExposedCardLexicalItemFromLearning:
          spaced_repetition_page_di.sl(),
      excludeUnspecifiedLexicalItemExposedCardLexicalItemFromLearning:
          spaced_repetition_page_di.sl(),
      exposeRepetitionAnswerSelectionObscureCard: spaced_repetition_page_di.sl(),
      exposeUnspecifiedLexicalItemObscureCard: spaced_repetition_page_di.sl(),
      handleAvailableEmailAppOuterUpdate: spaced_repetition_page_di.sl(),
      handleCachedImagePageMapOuterUpdate: spaced_repetition_page_di.sl(),
      handleDialectOuterUpdate: spaced_repetition_page_di.sl(),
      handleImageSettingsOuterUpdate: spaced_repetition_page_di.sl(),
      handleLearningContentOuterUpdate: spaced_repetition_page_di.sl(),
      handlePremiumAccessStatusOuterUpdate: spaced_repetition_page_di.sl(),
      handleRepetitionAnswerTypingCardCorrectAnswer: spaced_repetition_page_di.sl(),
      handleRepetitionAnswerTypingCardHintUsage: spaced_repetition_page_di.sl(),
      markRepetitionAnswerSelectionExposedCardLexicalItemAsBadlyRemembered:
          spaced_repetition_page_di.sl(),
      markRepetitionAnswerSelectionExposedCardLexicalItemAsCompletelyLearned:
          spaced_repetition_page_di.sl(),
      markRepetitionAnswerSelectionExposedCardLexicalItemAsRemembered:
          spaced_repetition_page_di.sl(),
      markRepetitionAnswerSelectionExposedCardLexicalItemAsUnremembered:
          spaced_repetition_page_di.sl(),
      markRepetitionAnswerTypingCardLexicalItemAsUnremembered: spaced_repetition_page_di.sl(),
      markUnspecifiedLexicalItemExposedCardLexicalItemAsKnown: spaced_repetition_page_di.sl(),
      markUnspecifiedLexicalItemExposedCardLexicalItemAsUnknown: spaced_repetition_page_di.sl(),
      maybeAutoPronounceMemorizationCardLexicalItem: spaced_repetition_page_di.sl(),
      maybeAutoPronounceRepetitionAnswerSelectionExposedCardLexicalItem:
          spaced_repetition_page_di.sl(),
      maybeAutoPronounceUnspecifiedLexicalItemObscureCardLexicalItem:
          spaced_repetition_page_di.sl(),
      reportMemorizationCardLexicalItemMistakeByEmail: spaced_repetition_page_di.sl(),
      reportRepetitionAnswerSelectionExposedCardLexicalItemMistakeByEmail:
          spaced_repetition_page_di.sl(),
      reportUnspecifiedLexicalItemExposedCardLexicalItemMistakeByEmail:
          spaced_repetition_page_di.sl(),
      resetMemorizationCardLexicalItemProgress: spaced_repetition_page_di.sl(),
      resetRepetitionAnswerSelectionExposedCardLexicalItemProgress: spaced_repetition_page_di.sl(),
      skipUnspecifiedLexicalItemExposedCard: spaced_repetition_page_di.sl(),
    );

    _bloc.streamWithPrevious.listen(_onBlocStateChanged);

    if (state.currentCardInfo is RepetitionAnswerTypingCardInfo) {
      currentTypingCardFocusNode = FocusNode();

      WidgetsBinding.instance.addPostFrameCallback((_) {
        currentTypingCardFocusNode!.requestFocus();
      });
    }

    _maybePerformBlocCurrentCardAction();

    cardChangeAnimationControllerNotifier = ValueNotifier(null);
    _setCardChangeAnimationController(null);

    nextCardScaleTween = Tween<double>(
      begin: 0.8,
      end: 1.0,
    );

    easeOutTween = CurveTween(
      curve: Curves.easeOut,
    );

    invertedTween = Tween<double>(
      begin: 1.0,
      end: 0.0,
    );

    leftSlideInTween = Tween<Offset>(
      begin: const Offset(1.0, 0.0),
      end: Offset.zero,
    );

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // clockwiseSlide animation
    ///////////////////////////////////////////////////////////////////////////////////////////////

    _clockwiseSlideAnimationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );

    clockwiseSlideAnimation = _clockwiseSlideAnimationController.drive(easeOutTween);

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // counterclockwiseSlide animation
    ///////////////////////////////////////////////////////////////////////////////////////////////

    _counterclockwiseSlideAnimationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );

    counterclockwiseSlideAnimation = _counterclockwiseSlideAnimationController.drive(easeOutTween);

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // crossFadeAnimation animation
    ///////////////////////////////////////////////////////////////////////////////////////////////

    _crossFadeAnimationController = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: this,
    );

    crossFadeOutAnimation = _crossFadeAnimationController.drive(easeOutTween).drive(invertedTween);

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // leftSlide animation
    ///////////////////////////////////////////////////////////////////////////////////////////////

    _leftSlideAnimationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );

    final leftSlideOutTween = Tween<Offset>(
      begin: Offset.zero,
      end: const Offset(-1.0, 0.0),
    );

    leftSlideOutAnimation =
        _leftSlideAnimationController.drive(easeOutTween).drive(leftSlideOutTween);

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // disappearance animation
    ///////////////////////////////////////////////////////////////////////////////////////////////

    _disappearanceAnimationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );

    const disappearanceFadeOutInterval = Interval(
      0.0,
      0.33,
      curve: Curves.easeOut,
    );

    disappearanceFadeOutAnimation = CurvedAnimation(
      curve: disappearanceFadeOutInterval,
      parent: _disappearanceAnimationController,
    ).drive(invertedTween);
  }

  @override
  void dispose() {
    _bloc.close();

    _clockwiseSlideAnimationController.dispose();
    _counterclockwiseSlideAnimationController.dispose();
    _crossFadeAnimationController.dispose();
    _leftSlideAnimationController.dispose();
    _disappearanceAnimationController.dispose();

    currentTypingCardFocusNode?.dispose();
    nextTypingCardFocusNode?.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return _SpacedRepetitionPageView(
      wloc: this,
    );
  }
}
